Mindomo search tools 程式碼

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta name="author" content="Lin Kuang Chang"> 
    <meta name="description" content="Mindomo search tools">
    <meta name="keywords" content="Mindomo,Mindmap">
    <meta name="revised" content="2025/11/21">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mindomo 檔案分享區</title>
    
    <!-- 設定瀏覽器分頁圖示 (Favicon) -->
    <link rel="icon" href="https://2blog.ilc.edu.tw/linkc/wp-content/uploads/sites/141/2024/12/linkc-logo.png" />
    
    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    
    <!-- React & ReactDOM -->
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    
    <!-- Babel -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

    <!-- Lucide Icons -->
    <script src="https://unpkg.com/lucide@latest"></script>
    
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700&display=swap');
        body {
            font-family: 'Noto Sans TC', sans-serif;
            background-color: #f3f4f6;
        }
        .card-hover:hover {
            transform: translateY(-2px);
            box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
        }
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(-5px); }
            to { opacity: 1; transform: translateY(0); }
        }
        .animate-fade-in {
            animation: fadeIn 0.3s ease-out forwards;
        }
        /* 自訂捲軸樣式 */
        .custom-scrollbar::-webkit-scrollbar {
            width: 6px;
        }
        .custom-scrollbar::-webkit-scrollbar-track {
            background: transparent;
        }
        .custom-scrollbar::-webkit-scrollbar-thumb {
            background-color: #cbd5e1;
            border-radius: 20px;
        }
        .appearance-none {
            -webkit-appearance: none;
            -moz-appearance: none;
            appearance: none;
        }
        /* Tooltip 樣式 */
        .tooltip {
            visibility: hidden;
            position: absolute;
            z-index: 50;
            right: 0;
            top: 100%;
            margin-top: 0.5rem;
            width: 280px;
            background-color: #1f2937;
            color: white;
            text-align: left;
            border-radius: 0.5rem;
            padding: 0.75rem;
            font-size: 0.75rem;
            opacity: 0;
            transition: opacity 0.3s;
            box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
        }
        .has-tooltip:hover .tooltip {
            visibility: visible;
            opacity: 1;
        }
    </style>
</head>
<body>
    <div id="root"></div>

    <script type="text/babel">
        const { useState, useEffect, useMemo } = React;

        // 定義主類別架構
        const PARA_ROOTS = ['00inbox', '01專案', '02領域', '03資源', '04檔案'];

        // 模擬資料
        const INITIAL_MOCK_DATA = [
            { id: "1", title: "2025 年度目標規劃", description: "包含 #工作 #學習 的年度大計畫 #share", modified_at: "2025-01-21T09:00:00Z" },
            { id: "2", title: "網站改版架構圖", description: "公司官網設計 #工作 #專案 #急件", modified_at: "2025-01-15T14:30:00Z" },
            { id: "3", title: "個人財務報表", description: "每季回顧 #健康 #財務", modified_at: "2025-01-10T11:00:00Z" },
            { id: "4", title: "Python 學習筆記", description: "爬蟲與數據分析 #學習 #程式 #Coding #share", modified_at: "2025-01-18T16:00:00Z" },
            { id: "5", title: "健身菜單", description: "重訓與飲食控制 #健康 #運動", modified_at: "2024-12-25T10:15:00Z" },
            { id: "6", title: "雜七雜八的想法", description: "還沒整理的靈感", modified_at: "2025-01-22T10:00:00Z" }, 
            { id: "7", title: "日本旅遊攻略", description: "行程安排 #旅遊 #休閒 #2025 #share", modified_at: "2024-11-05T09:00:00Z" },
            { id: "8", title: "讀書筆記:原子習慣", description: "#學習 #閱讀 #習慣 #share", modified_at: "2024-11-01T09:00:00Z" },
            { id: "9", title: "Q1 專案檢討", description: "#工作 #專案 #會議", modified_at: "2025-02-01T09:00:00Z" }
        ];

        const extractTags = (text) => {
            if (!text || typeof text !== 'string') return [];
            const cleanText = text.replace(/<[^>]*>?/gm, '');
            const matches = cleanText.match(/#[\S]+/g);
            if (!matches) return [];
            return matches.map(tag => tag.substring(1)).filter(t => t.length > 0);
        };
        
        const formatDate = (isoString) => {
            if (!isoString) return "無資料";
            const date = new Date(isoString);
            if (isNaN(date.getTime())) return "日期錯誤";
            return new Intl.DateTimeFormat('zh-TW', {
                year: 'numeric', month: '2-digit', day: '2-digit',
                hour: '2-digit', minute: '2-digit'
            }).format(date);
        };

        // Icons
        const Icons = {
            File: () => <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>,
            Clock: () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>,
            Tag: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2H2v10l9.29 9.29c.94.94 2.48.94 3.42 0l6.58-6.58c.94-.94.94-2.48 0-3.42L12 2Z"/><path d="M7 7h.01"/></svg>,
            Hash: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/></svg>,
            Grid: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>,
            Menu: () => <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>,
            Search: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>,
            Settings: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>,
            Refresh: ({ spin }) => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={spin ? "animate-spin" : ""}><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>,
            ChevronLeft: () => <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>,
            ChevronRight: () => <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>,
            ChevronDown: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>,
            SortAsc: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="M11 4h10"/><path d="M11 8h7"/><path d="M11 12h4"/></svg>,
            SortDesc: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/><path d="M11 12h4"/><path d="M11 16h7"/><path d="M11 20h10"/></svg>,
            ArrowDown: () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>,
            Bug: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>,
            AlignLeft: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="17" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="17" y1="18" x2="3" y2="18"/></svg>,
            Download: () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>,
            Info: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>,
            Filter: () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>,
            Lock: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>,
            Unlock: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>
        };

        const ITEMS_PER_PAGE = 9;
        // 設定硬寫入的 Token 與 密碼
        const HARDCODED_TOKEN = 'KgWdA3krt3dkuS3yQ0TWiOghaYeSGR_tp7u_SV9k_go';
        const ACCESS_PASSWORD = '590123';

        // 密碼輸入視窗
        const PasswordModal = ({ onSubmit, onClose }) => {
            const [input, setInput] = useState('');
            const [error, setError] = useState('');

            const handleSubmit = () => {
                if (input === ACCESS_PASSWORD) {
                    onSubmit();
                } else {
                    setError('密碼錯誤');
                }
            };

            return (
                <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 animate-fade-in p-4">
                    <div className="bg-white rounded-xl shadow-2xl w-full max-w-sm overflow-hidden p-6">
                        <h3 className="text-lg font-bold text-gray-800 mb-4">管理員權限驗證</h3>
                        <p className="text-sm text-gray-600 mb-4">請輸入密碼以查看所有檔案:</p>
                        <input 
                            type="password" 
                            value={input}
                            onChange={(e) => { setInput(e.target.value); setError(''); }}
                            onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
                            className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none mb-2"
                            placeholder="輸入密碼"
                            autoFocus
                        />
                        {error && <p className="text-xs text-red-500 mb-2">{error}</p>}
                        <div className="flex justify-end gap-2 mt-4">
                            <button onClick={onClose} className="px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 rounded-lg">取消</button>
                            <button onClick={handleSubmit} className="px-3 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg">確認</button>
                        </div>
                    </div>
                </div>
            );
        };

        const App = () => {
            // 使用硬寫入的 Token
            const [apiToken] = useState(HARDCODED_TOKEN);
            
            // 權限狀態:預設 false (訪客模式)
            const [isAuthorized, setIsAuthorized] = useState(false);
            const [showPasswordModal, setShowPasswordModal] = useState(false);

            const [diagrams, setDiagrams] = useState([]);
            const [loading, setLoading] = useState(false);
            const [error, setError] = useState(null);
            
            // 預設開啟 Real API 模式,因為 Token 已內建
            const [useRealApi, setUseRealApi] = useState(true);
            const [showDebug, setShowDebug] = useState(false);
            const [searchTerm, setSearchTerm] = useState('');
            const [rawData, setRawData] = useState(null);
            const [downloadingState, setDownloadingState] = useState(null);
            
            // 預設顯示 'all' (但在訪客模式下,底層資料已被過濾為 share)
            const [activeCategory, setActiveCategory] = useState('all'); 
            const [expandedFolders, setExpandedFolders] = useState(PARA_ROOTS.reduce((acc, root) => ({...acc, [root]: true}), {}));
            const [currentPage, setCurrentPage] = useState(1);
            const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
            const [tagSearchQuery, setTagSearchQuery] = useState('');
            const [isTagExpanded, setIsTagExpanded] = useState(false);
            const [sortField, setSortField] = useState('modified_at');
            const [sortOrder, setSortOrder] = useState('desc');

            // 初始化 Demo 資料 (僅當 useRealApi 為 false 時)
            useEffect(() => {
                if (!useRealApi) {
                     const normalizedMock = INITIAL_MOCK_DATA.map(item => ({
                         ...item,
                         tags: extractTags(item.description) 
                     }));
                     setDiagrams(normalizedMock);
                     setRawData(INITIAL_MOCK_DATA);
                } else {
                    // 若使用 Real API,組件掛載時直接抓取
                    fetchDiagrams();
                }
            }, [useRealApi]);

            useEffect(() => {
                setCurrentPage(1);
            }, [searchTerm, activeCategory, sortField, sortOrder]);

            const fetchDiagrams = async () => {
                if (!apiToken) { setError("程式碼中未設定 Token"); return; }
                setLoading(true); setError(null);
                setRawData(null);
                try {
                    const response = await fetch('https://www.mindomo.com/api/v1/diagrams', {
                        method: 'GET',
                        headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' }
                    });
                    if (!response.ok) {
                        if (response.status === 401) throw new Error("認證失敗:Token 無效");
                        if (response.status === 403) throw new Error("權限不足");
                        throw new Error(`API 錯誤: ${response.status}`);
                    }
                    const data = await response.json();
                    setRawData(data);
                    let items = Array.isArray(data) ? data : (data.diagrams || []);
                    
                    const normalizedData = items.map(item => {
                        const modDate = item.modified || item.lastModified || item.modified_at || item.updated || item.updated_at || item.dateModified || item.last_modified;
                        const createDate = item.created_at || item.creationDate || item.created || item.dateCreated;
                        const description = item.description || item.note || ""; 
                        const tags = extractTags(description);

                        return {
                            id: item.id,
                            title: item.title || item.name || "未命名檔案",
                            description: description,
                            tags: tags, 
                            created_at: createDate || new Date().toISOString(),
                            modified_at: modDate || createDate,
                            _raw: item
                        };
                    });
                    setDiagrams(normalizedData);
                } catch (err) {
                    console.error(err);
                    setError(err.message + " (若是 CORS 錯誤,請嘗試 Demo 模式)");
                } finally {
                    setLoading(false);
                }
            };

            const handleDownload = async (e, item, format) => {
                e.stopPropagation();
                if (!apiToken) { alert("API Token 設定錯誤"); return; }
                setDownloadingState({ id: item.id, format: format });
                try {
                    const apiFormat = format === 'md' ? 'txt' : format;
                    if (!useRealApi) {
                        await new Promise(resolve => setTimeout(resolve, 1000));
                        alert(`(Demo 模式) 模擬下載 ${item.title}.${format}`);
                        setDownloadingState(null);
                        return;
                    }
                    const response = await fetch(`https://www.mindomo.com/api/v1/diagrams/${item.id}.${apiFormat}`, {
                        headers: { 'Authorization': `Bearer ${apiToken}` }
                    });
                    if (!response.ok) throw new Error(`API 錯誤: ${response.status} ${response.statusText}`);
                    const blob = await response.blob();
                    const url = window.URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = `${item.title}.${format}`;
                    document.body.appendChild(a);
                    a.click();
                    window.URL.revokeObjectURL(url);
                    document.body.removeChild(a);
                } catch (err) {
                    console.error(err);
                    alert('下載失敗: ' + err.message);
                } finally {
                    setDownloadingState(null);
                }
            };

            // 權限過濾核心:如果未登入,只顯示包含 share 標籤的檔案
            const permittedDiagrams = useMemo(() => {
                if (isAuthorized) return diagrams;
                // 訪客模式:過濾含有 'share' 標籤的檔案 (不分大小寫)
                return diagrams.filter(item => 
                    item.tags && item.tags.some(t => t.toLowerCase() === 'share')
                );
            }, [diagrams, isAuthorized]);

            // 標籤統計 (基於 permittedDiagrams)
            const sortedTags = useMemo(() => {
                const tagsCount = {};
                permittedDiagrams.forEach(d => {
                    if (d.tags && d.tags.length > 0) {
                        d.tags.forEach(tag => {
                            tagsCount[tag] = (tagsCount[tag] || 0) + 1;
                        });
                    }
                });
                return Object.entries(tagsCount)
                    .sort(([, a], [, b]) => b - a) 
                    .map(([tag, count]) => ({ name: tag, count }));
            }, [permittedDiagrams]);

            const visibleTags = useMemo(() => {
                let tags = sortedTags;
                if (tagSearchQuery) {
                    tags = tags.filter(t => t.name.toLowerCase().includes(tagSearchQuery.toLowerCase()));
                }
                if (!isTagExpanded && !tagSearchQuery) {
                    return tags.slice(0, 10);
                }
                return tags;
            }, [sortedTags, tagSearchQuery, isTagExpanded]);

            const checkBooleanSearch = (item, query) => {
                try {
                    let evalStr = query.toLowerCase();
                    evalStr = evalStr.replace(/\s*&\s*/g, ' && ').replace(/\s*\|\s*/g, ' || ').replace(/\s*!\s*/g, ' ! ');
                    const itemTags = new Set((item.tags || []).map(t => t.toLowerCase()));
                    evalStr = evalStr.replace(/#([a-zA-Z0-9\u4e00-\u9fa5_]+)/g, (match, tagName) => {
                        return itemTags.has(tagName) ? 'true' : 'false';
                    });
                    if (!/^(true|false|&&|\|\||!|\(|\)|\s)+$/.test(evalStr)) return null;
                    return new Function(`return (${evalStr})`)();
                } catch (e) {
                    return null;
                }
            };

            const processedData = useMemo(() => {
                let baseList = [...permittedDiagrams];
                const trimmedTerm = searchTerm.trim();

                if (trimmedTerm) {
                    const isBooleanQuery = /[&|!]/.test(trimmedTerm) && /#/.test(trimmedTerm);
                    if (isBooleanQuery) {
                         baseList = baseList.filter(item => {
                             const result = checkBooleanSearch(item, trimmedTerm);
                             if (result === null) {
                                 const lower = trimmedTerm.toLowerCase();
                                 return item.title.toLowerCase().includes(lower) || 
                                        (item.description && item.description.toLowerCase().includes(lower)) ||
                                        (item.tags && item.tags.some(t => t.toLowerCase().includes(lower)));
                             }
                             return result;
                         });
                    } else {
                         const lower = trimmedTerm.toLowerCase();
                         baseList = baseList.filter(item => 
                            item.title.toLowerCase().includes(lower) || 
                            (item.description && item.description.toLowerCase().includes(lower)) ||
                            (item.tags && item.tags.some(t => t.toLowerCase().includes(lower)))
                        );
                    }
                }
                
                let filtered = [];
                if (activeCategory === 'recent') {
                    filtered = [...baseList]
                        .filter(item => item.modified_at)
                        .sort((a, b) => new Date(b.modified_at) - new Date(a.modified_at))
                        .slice(0, 20);
                } else if (activeCategory === 'all') {
                    filtered = baseList;
                } else if (activeCategory === 'uncategorized') {
                    filtered = baseList.filter(item => !item.tags || item.tags.length === 0);
                } else {
                    filtered = baseList.filter(item => item.tags && item.tags.includes(activeCategory));
                }

                if (activeCategory !== 'recent') {
                    filtered.sort((a, b) => {
                        let valA = a[sortField];
                        let valB = b[sortField];
                        if (!valA && !valB) return 0;
                        if (!valA) return 1;
                        if (!valB) return -1;
                        let comparison = 0;
                        if (sortField === 'title') comparison = valA.localeCompare(valB, 'zh-Hant');
                        else comparison = new Date(valA) - new Date(valB);
                        return sortOrder === 'asc' ? comparison : -comparison;
                    });
                }
                const totalItems = filtered.length;
                const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE);
                const items = filtered.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE);
                return { items, totalItems, totalPages };
            }, [permittedDiagrams, searchTerm, activeCategory, currentPage, sortField, sortOrder]);

            const Pagination = () => {
                if (processedData.totalPages <= 1) return null;
                return (
                    <div className="flex justify-center items-center gap-2 mt-8 pt-6 border-t border-gray-200">
                        <button onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="p-2 rounded-md hover:bg-gray-100 disabled:opacity-30"><Icons.ChevronLeft /></button>
                        <span className="text-sm text-gray-600">第 {currentPage} / {processedData.totalPages} 頁</span>
                        <button onClick={() => setCurrentPage(p => Math.min(processedData.totalPages, p + 1))} disabled={currentPage === processedData.totalPages} className="p-2 rounded-md hover:bg-gray-100 disabled:opacity-30"><Icons.ChevronRight /></button>
                    </div>
                );
            };

            const SidebarItem = ({ id, label, icon, count, isActiveOverride }) => (
                <button onClick={() => { setActiveCategory(id); setIsMobileMenuOpen(false); }} className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors mb-1 ${(activeCategory === id || isActiveOverride) ? 'bg-blue-50 text-blue-700' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'}`}>
                    {icon}
                    <span className="flex-grow text-left truncate">{label}</span>
                    {count !== undefined && <span className="text-xs text-gray-400 bg-white px-1.5 py-0.5 rounded border border-gray-100">{count}</span>}
                </button>
            );

            return (
                <div className="min-h-screen bg-gray-50 flex flex-col">
                    <header className="bg-white border-b border-gray-200 sticky top-0 z-30 h-16 flex items-center px-4 justify-between lg:hidden">
                        <div className="flex items-center gap-2">
                            <img src="https://2blog.ilc.edu.tw/linkc/wp-content/uploads/sites/141/2024/12/linkc-logo.png" alt="Logo" className="h-10 w-10 rounded-lg object-contain" />
                            <h1 className="font-bold text-gray-800">Mindomo Tags</h1>
                        </div>
                        <button onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} className="p-2 text-gray-600 hover:bg-gray-100 rounded-md"><Icons.Menu /></button>
                    </header>

                    <div className="flex flex-1 overflow-hidden relative">
                        <aside className={`fixed inset-y-0 left-0 z-20 w-64 bg-white border-r border-gray-200 transform transition-transform duration-200 ease-in-out lg:translate-x-0 lg:static lg:h-auto ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'}`}>
                            <div className="h-full flex flex-col">
                                <div className="hidden lg:flex h-16 items-center px-6 border-b border-gray-100">
                                    <div className="flex items-center gap-3 text-blue-700">
                                        <img src="https://2blog.ilc.edu.tw/linkc/wp-content/uploads/sites/141/2024/12/linkc-logo.png" alt="Logo" className="h-10 w-10 rounded-lg object-contain" />
                                        <span className="font-bold text-xl">Mindomo</span>
                                    </div>
                                </div>
                                <div className="flex-1 overflow-y-auto custom-scrollbar p-4">
                                    <div className="mb-6">
                                        <p className="px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">儀表板</p>
                                        <SidebarItem id="recent" label="最近開啟" icon={<Icons.Clock />} />
                                        <SidebarItem id="all" label="全部檔案" icon={<Icons.Grid />} count={permittedDiagrams.length} />
                                    </div>
                                    
                                    <div className="mb-6">
                                        <div className="flex items-center justify-between px-3 mb-2">
                                            <p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">標籤分類 (#Tag)</p>
                                            {(isTagExpanded || sortedTags.length > 8) && (
                                                <div className="relative">
                                                    <input 
                                                        type="text" 
                                                        placeholder="篩選..." 
                                                        value={tagSearchQuery}
                                                        onChange={(e) => setTagSearchQuery(e.target.value)}
                                                        className="w-24 text-xs bg-gray-100 border border-transparent focus:bg-white focus:border-blue-300 rounded px-1.5 py-0.5 outline-none transition-colors"
                                                    />
                                                </div>
                                            )}
                                        </div>

                                        <div className="px-3 flex flex-wrap gap-2">
                                            {visibleTags.length === 0 && <p className="text-xs text-gray-400 italic w-full">沒有發現標籤</p>}
                                            {visibleTags.map(tag => {
                                                const isActive = activeCategory === tag.name;
                                                return (
                                                    <button 
                                                        key={tag.name}
                                                        onClick={() => { setActiveCategory(tag.name); setIsMobileMenuOpen(false); }}
                                                        className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium transition-colors border ${
                                                            isActive 
                                                                ? 'bg-blue-600 text-white border-blue-600 shadow-sm' 
                                                                : 'bg-white text-gray-600 border-gray-200 hover:border-blue-300 hover:text-blue-600'
                                                        }`}
                                                        title={`${tag.name} (${tag.count})`}
                                                    >
                                                        <span className="truncate max-w-[80px]">{tag.name}</span>
                                                        <span className={`ml-1.5 text-[10px] ${isActive ? 'text-blue-100' : 'text-gray-400'}`}>{tag.count}</span>
                                                    </button>
                                                );
                                            })}
                                        </div>
                                        
                                        {sortedTags.length > 10 && !tagSearchQuery && (
                                            <button 
                                                onClick={() => setIsTagExpanded(!isTagExpanded)}
                                                className="w-full text-center text-xs text-blue-500 hover:text-blue-700 mt-2 py-1 transition-colors"
                                            >
                                                {isTagExpanded ? '收合標籤' : `顯示全部 (${sortedTags.length})`}
                                            </button>
                                        )}
                                    </div>

                                    <div>
                                        <p className="px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">其他</p>
                                        <SidebarItem id="uncategorized" label="未分類 (無標籤)" icon={<Icons.File />} count={permittedDiagrams.filter(d => !d.tags || d.tags.length === 0).length} />
                                    </div>
                                </div>
                                <div className="p-4 border-t border-gray-100 bg-gray-50 space-y-2">
                                    {/* 權限切換按鈕 */}
                                    <button 
                                        onClick={() => isAuthorized ? setIsAuthorized(false) : setShowPasswordModal(true)} 
                                        className={`w-full flex items-center justify-center gap-2 text-xs py-2 px-2 border rounded transition-colors ${isAuthorized ? 'bg-red-50 border-red-200 text-red-600 hover:bg-red-100' : 'bg-white border-gray-300 text-gray-600 hover:bg-gray-50'}`}
                                    >
                                        {isAuthorized ? <Icons.Unlock /> : <Icons.Lock />}
                                        {isAuthorized ? '登出管理員' : '管理員登入'}
                                    </button>

                                    <button onClick={() => setUseRealApi(!useRealApi)} className="w-full text-xs py-1.5 px-2 border border-gray-300 rounded bg-white text-gray-600 hover:bg-gray-50">
                                        {useRealApi ? '切換演示模式' : '切換 API 模式'}
                                    </button>
                                    
                                    {useRealApi && isAuthorized && (
                                        <div className="grid grid-cols-1 gap-2">
                                            <button onClick={() => setShowDebug(!showDebug)} className={`flex items-center justify-center gap-1 text-xs py-1.5 px-2 border border-gray-200 rounded bg-white transition-colors ${showDebug ? 'text-green-600 bg-green-50 border-green-200' : 'text-gray-500 hover:text-gray-700'}`}>
                                                <Icons.Bug />
                                                {showDebug ? '關閉除錯' : '除錯模式'}
                                            </button>
                                        </div>
                                    )}
                                </div>
                            </div>
                        </aside>
                        {isMobileMenuOpen && <div onClick={() => setIsMobileMenuOpen(false)} className="fixed inset-0 z-10 bg-gray-800 bg-opacity-50 lg:hidden"></div>}

                        <main className="flex-1 overflow-y-auto custom-scrollbar h-full bg-gray-50/50 w-full">
                            <div className="max-w-5xl mx-auto px-4 sm:px-8 py-8">
                                <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
                                    <div>
                                        <div className="flex items-center gap-2 mb-1">
                                            <h2 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
                                                {activeCategory === 'recent' ? '最近修改' : 
                                                 activeCategory === 'all' ? '全部檔案' : 
                                                 activeCategory === 'uncategorized' ? '未分類檔案' : 
                                                 `# ${activeCategory}`}
                                            </h2>
                                            {!isAuthorized && (
                                                <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
                                                    訪客模式 (#share)
                                                </span>
                                            )}
                                        </div>
                                        <p className="text-sm text-gray-500">共 {processedData.totalItems} 個項目</p>
                                    </div>
                                    <div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
                                        <div className="relative group flex-1">
                                            <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"><Icons.Search /></div>
                                            <input 
                                                type="text" 
                                                value={searchTerm} 
                                                onChange={(e) => setSearchTerm(e.target.value)} 
                                                className="block w-full pl-10 pr-10 py-2 border border-gray-200 rounded-lg bg-white focus:ring-2 focus:ring-blue-500 outline-none text-sm" 
                                                placeholder="搜尋... (如 #工作 & #急件)" 
                                            />
                                            <div className="absolute inset-y-0 right-0 pr-3 flex items-center cursor-help has-tooltip">
                                                <div className="text-gray-400 hover:text-blue-500"><Icons.Info /></div>
                                                <div className="tooltip">
                                                    <p className="font-bold mb-1 text-blue-300">進階標籤搜尋語法:</p>
                                                    <ul className="list-disc list-inside space-y-1">
                                                        <li><code className="bg-gray-700 px-1 rounded">&</code> (AND): 同時包含</li>
                                                        <li><code className="bg-gray-700 px-1 rounded">|</code> (OR): 包含任一</li>
                                                        <li><code className="bg-gray-700 px-1 rounded">!</code> (NOT): 排除</li>
                                                        <li><code className="bg-gray-700 px-1 rounded">()</code>: 優先權</li>
                                                    </ul>
                                                </div>
                                            </div>
                                        </div>
                                        {activeCategory !== 'recent' && (
                                            <>
                                                <div className="h-8 w-px bg-gray-300 hidden sm:block mx-1"></div>
                                                <div className="flex gap-2">
                                                    <div className="relative">
                                                        <select value={sortField} onChange={(e) => setSortField(e.target.value)} className="appearance-none w-full sm:w-32 bg-white border border-gray-200 text-gray-700 text-sm py-2 pl-3 pr-8 rounded-lg focus:ring-2 focus:ring-blue-500 cursor-pointer outline-none">
                                                            <option value="modified_at">最近修改</option>
                                                            <option value="created_at">創建時間</option>
                                                            <option value="title">檔案名稱</option>
                                                        </select>
                                                        <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-500"><Icons.ArrowDown /></div>
                                                    </div>
                                                    <button onClick={() => setSortOrder(o => o === 'asc' ? 'desc' : 'asc')} className="p-2 bg-white border border-gray-200 rounded-lg text-gray-600 hover:text-blue-600">{sortOrder === 'asc' ? <Icons.SortAsc /> : <Icons.SortDesc />}</button>
                                                </div>
                                            </>
                                        )}
                                        {useRealApi && <button onClick={fetchDiagrams} disabled={loading} className="p-2 bg-white border border-gray-200 rounded-lg text-blue-600 hover:bg-blue-50"><Icons.Refresh spin={loading} /></button>}
                                    </div>
                                </div>
                                
                                {/* Main Content Area - Grid/Cards */}
                                {error && <div className="mb-6 p-4 bg-red-50 text-red-700 rounded-lg border border-red-100">{error}</div>}
                                
                                {processedData.totalItems === 0 ? (
                                    <div className="text-center py-20 animate-fade-in">
                                        <div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gray-100 mb-4 text-gray-300"><Icons.Grid /></div>
                                        <h3 className="text-lg font-medium text-gray-900">沒有檔案</h3>
                                        <p className="text-gray-500 mt-2 text-sm">
                                            {isAuthorized ? "確認是否有在 Mindomo 描述欄位加上 #標籤 ?" : "目前沒有公開分享 (#share) 的檔案"}
                                        </p>
                                    </div>
                                ) : (
                                    <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
                                        {processedData.items.map((item) => (
                                            <div key={item.id} className="group bg-white rounded-xl border border-gray-200 p-5 transition-all duration-200 card-hover flex flex-col h-full relative overflow-hidden">
                                                <div className="absolute top-0 left-0 w-1 h-full bg-blue-500 opacity-0 group-hover:opacity-100 transition-opacity"></div>
                                                <div className="flex justify-between items-start mb-3">
                                                    <div className="p-2.5 bg-blue-50 text-blue-600 rounded-lg group-hover:bg-blue-600 group-hover:text-white transition-colors"><Icons.File /></div>
                                                    <div className="flex gap-2">
                                                        {useRealApi && <a href={`https://www.mindomo.com/mindmap/${item.id}`} target="_blank" className="text-xs font-medium text-blue-600 bg-blue-50 px-2.5 py-1.5 rounded-full hover:bg-blue-100 transition-colors flex items-center">開啟</a>}
                                                    </div>
                                                </div>
                                                
                                                <h3 className="text-lg font-bold text-gray-900 mb-2 line-clamp-2 leading-snug group-hover:text-blue-600 transition-colors">{item.title}</h3>
                                                
                                                {/* 顯示標籤區塊 */}
                                                <div className="flex flex-wrap gap-1 mb-4 min-h-[24px]">
                                                    {item.tags && item.tags.length > 0 ? (
                                                        item.tags.map(tag => (
                                                            <span key={tag} onClick={(e) => { e.stopPropagation(); setActiveCategory(tag); }} className="cursor-pointer inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 hover:bg-blue-100 hover:text-blue-600 transition-colors">
                                                                #{tag}
                                                            </span>
                                                        ))
                                                    ) : (
                                                        <span className="text-xs text-gray-400 italic">無標籤</span>
                                                    )}
                                                </div>

                                                <div className="mt-auto pt-4 border-t border-gray-50 space-y-2">
                                                    <div className="flex items-center text-xs text-gray-500">
                                                        <Icons.Clock />
                                                        <span className="ml-2">
                                                            {item.modified_at ? `最後修改於 ${formatDate(item.modified_at)}` : '無修改日期'}
                                                        </span>
                                                    </div>
                                                    
                                                    <div className="flex gap-2 pt-2 border-t border-gray-100 justify-end">
                                                        <button onClick={(e) => handleDownload(e, item, 'md')} disabled={downloadingState && downloadingState.id === item.id} className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 bg-gray-50 hover:bg-gray-100 rounded transition-colors disabled:opacity-50" title="下載 Markdown (.md)">{downloadingState && downloadingState.id === item.id && downloadingState.format === 'md' ? <Icons.Refresh spin={true} /> : <span>MD</span>}</button>
                                                        <button onClick={(e) => handleDownload(e, item, 'mm')} disabled={downloadingState && downloadingState.id === item.id} className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 bg-gray-50 hover:bg-gray-100 rounded transition-colors disabled:opacity-50" title="下載 Freemind (.mm)">{downloadingState && downloadingState.id === item.id && downloadingState.format === 'mm' ? <Icons.Refresh spin={true} /> : <span>MM</span>}</button>
                                                        <button onClick={(e) => handleDownload(e, item, 'pdf')} disabled={downloadingState && downloadingState.id === item.id} className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 bg-gray-50 hover:bg-gray-100 rounded transition-colors disabled:opacity-50" title="下載 PDF (.pdf)">{downloadingState && downloadingState.id === item.id && downloadingState.format === 'pdf' ? <Icons.Refresh spin={true} /> : <span>PDF</span>}</button>
                                                    </div>
                                                </div>
                                            </div>
                                        ))}
                                    </div>
                                )}
                                <Pagination />
                                
                                {/* 密碼輸入視窗 */}
                                {showPasswordModal && (
                                    <PasswordModal 
                                        onSubmit={() => {
                                            setIsAuthorized(true);
                                            setShowPasswordModal(false);
                                        }}
                                        onClose={() => setShowPasswordModal(false)}
                                    />
                                )}

                                {useRealApi && rawData && showDebug && (
                                    <div className="mt-16 p-6 bg-gray-900 text-gray-300 rounded-xl shadow-lg animate-fade-in">
                                        <div className="flex items-center justify-between mb-4 border-b border-gray-700 pb-4">
                                            <div className="flex items-center gap-2">
                                                <Icons.Bug />
                                                <h3 className="text-lg font-bold text-white">API 資料除錯 (Debug Info)</h3>
                                            </div>
                                            <button onClick={() => setShowDebug(false)} className="text-xs text-gray-500 hover:text-white">關閉</button>
                                        </div>
                                        <p className="text-sm mb-4 text-gray-400">
                                            請檢查 JSON 中 <code>description</code> 欄位是否有包含 #標籤。
                                        </p>
                                        
                                        <div className="space-y-6">
                                            <div>
                                                <h4 className="text-xs font-bold text-blue-400 uppercase tracking-wider mb-2">原始資料 (前 1 筆範例)</h4>
                                                <pre className="bg-black p-4 rounded-lg text-xs overflow-x-auto font-mono border border-gray-800 text-green-400">
                                                    {JSON.stringify(Array.isArray(rawData) ? rawData[0] : (rawData.diagrams ? rawData.diagrams[0] : rawData), null, 2)}
                                                </pre>
                                            </div>
                                            
                                            <div>
                                                <h4 className="text-xs font-bold text-blue-400 uppercase tracking-wider mb-2">資料結構統計</h4>
                                                <div className="bg-black p-4 rounded-lg text-xs font-mono border border-gray-800">
                                                    <p>總筆數: {Array.isArray(rawData) ? rawData.length : (rawData.diagrams ? rawData.diagrams.length : 0)}</p>
                                                    <p>包含 'description' 欄位的筆數: {(Array.isArray(rawData) ? rawData : (rawData.diagrams || [])).filter(i => i.description).length}</p>
                                                    <p>偵測到的標籤總數: {sortedTags.length}</p>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                )}
                            </div>
                        </main>
                    </div>
                </div>
            );
        }

        const root = ReactDOM.createRoot(document.getElementById('root'));
        root.render(<App />);
    </script>
</body>
</html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mindomo 檔案分享區</title>
    
    <!-- 設定瀏覽器分頁圖示 (Favicon) -->
    <link rel="icon" href="https://2blog.ilc.edu.tw/linkc/wp-content/uploads/sites/141/2024/12/linkc-logo.png" />
    
    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    
    <!-- React & ReactDOM -->
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    
    <!-- Babel -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

    <!-- Lucide Icons -->
    <script src="https://unpkg.com/lucide@latest"></script>
    
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700&display=swap');
        body {
            font-family: 'Noto Sans TC', sans-serif;
            background-color: #f3f4f6;
        }
        .card-hover:hover {
            transform: translateY(-2px);
            box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
        }
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(-5px); }
            to { opacity: 1; transform: translateY(0); }
        }
        .animate-fade-in {
            animation: fadeIn 0.3s ease-out forwards;
        }
        /* 自訂捲軸樣式 */
        .custom-scrollbar::-webkit-scrollbar {
            width: 6px;
        }
        .custom-scrollbar::-webkit-scrollbar-track {
            background: transparent;
        }
        .custom-scrollbar::-webkit-scrollbar-thumb {
            background-color: #cbd5e1;
            border-radius: 20px;
        }
        .appearance-none {
            -webkit-appearance: none;
            -moz-appearance: none;
            appearance: none;
        }
        /* Tooltip 樣式 */
        .tooltip {
            visibility: hidden;
            position: absolute;
            z-index: 50;
            right: 0;
            top: 100%;
            margin-top: 0.5rem;
            width: 280px;
            background-color: #1f2937;
            color: white;
            text-align: left;
            border-radius: 0.5rem;
            padding: 0.75rem;
            font-size: 0.75rem;
            opacity: 0;
            transition: opacity 0.3s;
            box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
        }
        .has-tooltip:hover .tooltip {
            visibility: visible;
            opacity: 1;
        }
    </style>
</head>
<body>
    <div id="root"></div>

    <script type="text/babel">
        const { useState, useEffect, useMemo } = React;

        // 定義主類別架構
        const PARA_ROOTS = ['00inbox', '01專案', '02領域', '03資源', '04檔案'];

        // 模擬資料
        const INITIAL_MOCK_DATA = [
            { id: "1", title: "2025 年度目標規劃", description: "包含 #工作 #學習 的年度大計畫 #share", modified_at: "2025-01-21T09:00:00Z" },
            { id: "2", title: "網站改版架構圖", description: "公司官網設計 #工作 #專案 #急件", modified_at: "2025-01-15T14:30:00Z" },
            { id: "3", title: "個人財務報表", description: "每季回顧 #健康 #財務", modified_at: "2025-01-10T11:00:00Z" },
            { id: "4", title: "Python 學習筆記", description: "爬蟲與數據分析 #學習 #程式 #Coding #share", modified_at: "2025-01-18T16:00:00Z" },
            { id: "5", title: "健身菜單", description: "重訓與飲食控制 #健康 #運動", modified_at: "2024-12-25T10:15:00Z" },
            { id: "6", title: "雜七雜八的想法", description: "還沒整理的靈感", modified_at: "2025-01-22T10:00:00Z" }, 
            { id: "7", title: "日本旅遊攻略", description: "行程安排 #旅遊 #休閒 #2025 #share", modified_at: "2024-11-05T09:00:00Z" },
            { id: "8", title: "讀書筆記:原子習慣", description: "#學習 #閱讀 #習慣 #share", modified_at: "2024-11-01T09:00:00Z" },
            { id: "9", title: "Q1 專案檢討", description: "#工作 #專案 #會議", modified_at: "2025-02-01T09:00:00Z" }
        ];

        const extractTags = (text) => {
            if (!text || typeof text !== 'string') return [];
            const cleanText = text.replace(/<[^>]*>?/gm, '');
            const matches = cleanText.match(/#[\S]+/g);
            if (!matches) return [];
            return matches.map(tag => tag.substring(1)).filter(t => t.length > 0);
        };
        
        const formatDate = (isoString) => {
            if (!isoString) return "無資料";
            const date = new Date(isoString);
            if (isNaN(date.getTime())) return "日期錯誤";
            return new Intl.DateTimeFormat('zh-TW', {
                year: 'numeric', month: '2-digit', day: '2-digit',
                hour: '2-digit', minute: '2-digit'
            }).format(date);
        };

        // Icons
        const Icons = {
            File: () => <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>,
            Clock: () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>,
            Tag: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2H2v10l9.29 9.29c.94.94 2.48.94 3.42 0l6.58-6.58c.94-.94.94-2.48 0-3.42L12 2Z"/><path d="M7 7h.01"/></svg>,
            Hash: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/></svg>,
            Grid: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>,
            Menu: () => <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>,
            Search: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>,
            Settings: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>,
            Refresh: ({ spin }) => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={spin ? "animate-spin" : ""}><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>,
            ChevronLeft: () => <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>,
            ChevronRight: () => <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>,
            ChevronDown: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>,
            SortAsc: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="M11 4h10"/><path d="M11 8h7"/><path d="M11 12h4"/></svg>,
            SortDesc: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/><path d="M11 12h4"/><path d="M11 16h7"/><path d="M11 20h10"/></svg>,
            ArrowDown: () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>,
            Bug: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>,
            AlignLeft: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="17" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="17" y1="18" x2="3" y2="18"/></svg>,
            Download: () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>,
            Info: () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>,
            Filter: () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>,
            Lock: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>,
            Unlock: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>
        };

        const ITEMS_PER_PAGE = 9;
        // 設定硬寫入的 Token 與 密碼
        const HARDCODED_TOKEN = 'KgWdA3krt3dkuS3yQ0TWiOghaYeSGR_tp7u_SV9k_go';
        const ACCESS_PASSWORD = '590123';

        // 密碼輸入視窗
        const PasswordModal = ({ onSubmit, onClose }) => {
            const [input, setInput] = useState('');
            const [error, setError] = useState('');

            const handleSubmit = () => {
                if (input === ACCESS_PASSWORD) {
                    onSubmit();
                } else {
                    setError('密碼錯誤');
                }
            };

            return (
                <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 animate-fade-in p-4">
                    <div className="bg-white rounded-xl shadow-2xl w-full max-w-sm overflow-hidden p-6">
                        <h3 className="text-lg font-bold text-gray-800 mb-4">管理員權限驗證</h3>
                        <p className="text-sm text-gray-600 mb-4">請輸入密碼以查看所有檔案:</p>
                        <input 
                            type="password" 
                            value={input}
                            onChange={(e) => { setInput(e.target.value); setError(''); }}
                            onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
                            className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none mb-2"
                            placeholder="輸入密碼"
                            autoFocus
                        />
                        {error && <p className="text-xs text-red-500 mb-2">{error}</p>}
                        <div className="flex justify-end gap-2 mt-4">
                            <button onClick={onClose} className="px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 rounded-lg">取消</button>
                            <button onClick={handleSubmit} className="px-3 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg">確認</button>
                        </div>
                    </div>
                </div>
            );
        };

        const App = () => {
            // 使用硬寫入的 Token
            const [apiToken] = useState(HARDCODED_TOKEN);
            
            // 權限狀態:預設 false (訪客模式)
            const [isAuthorized, setIsAuthorized] = useState(false);
            const [showPasswordModal, setShowPasswordModal] = useState(false);

            const [diagrams, setDiagrams] = useState([]);
            const [loading, setLoading] = useState(false);
            const [error, setError] = useState(null);
            
            // 預設開啟 Real API 模式,因為 Token 已內建
            const [useRealApi, setUseRealApi] = useState(true);
            const [showDebug, setShowDebug] = useState(false);
            const [searchTerm, setSearchTerm] = useState('');
            const [rawData, setRawData] = useState(null);
            const [downloadingState, setDownloadingState] = useState(null);
            
            // 預設顯示 'all' (但在訪客模式下,底層資料已被過濾為 share)
            const [activeCategory, setActiveCategory] = useState('all'); 
            const [expandedFolders, setExpandedFolders] = useState(PARA_ROOTS.reduce((acc, root) => ({...acc, [root]: true}), {}));
            const [currentPage, setCurrentPage] = useState(1);
            const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
            const [tagSearchQuery, setTagSearchQuery] = useState('');
            const [isTagExpanded, setIsTagExpanded] = useState(false);
            const [sortField, setSortField] = useState('modified_at');
            const [sortOrder, setSortOrder] = useState('desc');

            // 初始化 Demo 資料 (僅當 useRealApi 為 false 時)
            useEffect(() => {
                if (!useRealApi) {
                     const normalizedMock = INITIAL_MOCK_DATA.map(item => ({
                         ...item,
                         tags: extractTags(item.description) 
                     }));
                     setDiagrams(normalizedMock);
                     setRawData(INITIAL_MOCK_DATA);
                } else {
                    // 若使用 Real API,組件掛載時直接抓取
                    fetchDiagrams();
                }
            }, [useRealApi]);

            useEffect(() => {
                setCurrentPage(1);
            }, [searchTerm, activeCategory, sortField, sortOrder]);

            const fetchDiagrams = async () => {
                if (!apiToken) { setError("程式碼中未設定 Token"); return; }
                setLoading(true); setError(null);
                setRawData(null);
                try {
                    const response = await fetch('https://www.mindomo.com/api/v1/diagrams', {
                        method: 'GET',
                        headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' }
                    });
                    if (!response.ok) {
                        if (response.status === 401) throw new Error("認證失敗:Token 無效");
                        if (response.status === 403) throw new Error("權限不足");
                        throw new Error(`API 錯誤: ${response.status}`);
                    }
                    const data = await response.json();
                    setRawData(data);
                    let items = Array.isArray(data) ? data : (data.diagrams || []);
                    
                    const normalizedData = items.map(item => {
                        const modDate = item.modified || item.lastModified || item.modified_at || item.updated || item.updated_at || item.dateModified || item.last_modified;
                        const createDate = item.created_at || item.creationDate || item.created || item.dateCreated;
                        const description = item.description || item.note || ""; 
                        const tags = extractTags(description);

                        return {
                            id: item.id,
                            title: item.title || item.name || "未命名檔案",
                            description: description,
                            tags: tags, 
                            created_at: createDate || new Date().toISOString(),
                            modified_at: modDate || createDate,
                            _raw: item
                        };
                    });
                    setDiagrams(normalizedData);
                } catch (err) {
                    console.error(err);
                    setError(err.message + " (若是 CORS 錯誤,請嘗試 Demo 模式)");
                } finally {
                    setLoading(false);
                }
            };

            const handleDownload = async (e, item, format) => {
                e.stopPropagation();
                if (!apiToken) { alert("API Token 設定錯誤"); return; }
                setDownloadingState({ id: item.id, format: format });
                try {
                    const apiFormat = format === 'md' ? 'txt' : format;
                    if (!useRealApi) {
                        await new Promise(resolve => setTimeout(resolve, 1000));
                        alert(`(Demo 模式) 模擬下載 ${item.title}.${format}`);
                        setDownloadingState(null);
                        return;
                    }
                    const response = await fetch(`https://www.mindomo.com/api/v1/diagrams/${item.id}.${apiFormat}`, {
                        headers: { 'Authorization': `Bearer ${apiToken}` }
                    });
                    if (!response.ok) throw new Error(`API 錯誤: ${response.status} ${response.statusText}`);
                    const blob = await response.blob();
                    const url = window.URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = `${item.title}.${format}`;
                    document.body.appendChild(a);
                    a.click();
                    window.URL.revokeObjectURL(url);
                    document.body.removeChild(a);
                } catch (err) {
                    console.error(err);
                    alert('下載失敗: ' + err.message);
                } finally {
                    setDownloadingState(null);
                }
            };

            // 權限過濾核心:如果未登入,只顯示包含 share 標籤的檔案
            const permittedDiagrams = useMemo(() => {
                if (isAuthorized) return diagrams;
                // 訪客模式:過濾含有 'share' 標籤的檔案 (不分大小寫)
                return diagrams.filter(item => 
                    item.tags && item.tags.some(t => t.toLowerCase() === 'share')
                );
            }, [diagrams, isAuthorized]);

            // 標籤統計 (基於 permittedDiagrams)
            const sortedTags = useMemo(() => {
                const tagsCount = {};
                permittedDiagrams.forEach(d => {
                    if (d.tags && d.tags.length > 0) {
                        d.tags.forEach(tag => {
                            tagsCount[tag] = (tagsCount[tag] || 0) + 1;
                        });
                    }
                });
                return Object.entries(tagsCount)
                    .sort(([, a], [, b]) => b - a) 
                    .map(([tag, count]) => ({ name: tag, count }));
            }, [permittedDiagrams]);

            const visibleTags = useMemo(() => {
                let tags = sortedTags;
                if (tagSearchQuery) {
                    tags = tags.filter(t => t.name.toLowerCase().includes(tagSearchQuery.toLowerCase()));
                }
                if (!isTagExpanded && !tagSearchQuery) {
                    return tags.slice(0, 10);
                }
                return tags;
            }, [sortedTags, tagSearchQuery, isTagExpanded]);

            const checkBooleanSearch = (item, query) => {
                try {
                    let evalStr = query.toLowerCase();
                    evalStr = evalStr.replace(/\s*&\s*/g, ' && ').replace(/\s*\|\s*/g, ' || ').replace(/\s*!\s*/g, ' ! ');
                    const itemTags = new Set((item.tags || []).map(t => t.toLowerCase()));
                    evalStr = evalStr.replace(/#([a-zA-Z0-9\u4e00-\u9fa5_]+)/g, (match, tagName) => {
                        return itemTags.has(tagName) ? 'true' : 'false';
                    });
                    if (!/^(true|false|&&|\|\||!|\(|\)|\s)+$/.test(evalStr)) return null;
                    return new Function(`return (${evalStr})`)();
                } catch (e) {
                    return null;
                }
            };

            const processedData = useMemo(() => {
                let baseList = [...permittedDiagrams];
                const trimmedTerm = searchTerm.trim();

                if (trimmedTerm) {
                    const isBooleanQuery = /[&|!]/.test(trimmedTerm) && /#/.test(trimmedTerm);
                    if (isBooleanQuery) {
                         baseList = baseList.filter(item => {
                             const result = checkBooleanSearch(item, trimmedTerm);
                             if (result === null) {
                                 const lower = trimmedTerm.toLowerCase();
                                 return item.title.toLowerCase().includes(lower) || 
                                        (item.description && item.description.toLowerCase().includes(lower)) ||
                                        (item.tags && item.tags.some(t => t.toLowerCase().includes(lower)));
                             }
                             return result;
                         });
                    } else {
                         const lower = trimmedTerm.toLowerCase();
                         baseList = baseList.filter(item => 
                            item.title.toLowerCase().includes(lower) || 
                            (item.description && item.description.toLowerCase().includes(lower)) ||
                            (item.tags && item.tags.some(t => t.toLowerCase().includes(lower)))
                        );
                    }
                }
                
                let filtered = [];
                if (activeCategory === 'recent') {
                    filtered = [...baseList]
                        .filter(item => item.modified_at)
                        .sort((a, b) => new Date(b.modified_at) - new Date(a.modified_at))
                        .slice(0, 20);
                } else if (activeCategory === 'all') {
                    filtered = baseList;
                } else if (activeCategory === 'uncategorized') {
                    filtered = baseList.filter(item => !item.tags || item.tags.length === 0);
                } else {
                    filtered = baseList.filter(item => item.tags && item.tags.includes(activeCategory));
                }

                if (activeCategory !== 'recent') {
                    filtered.sort((a, b) => {
                        let valA = a[sortField];
                        let valB = b[sortField];
                        if (!valA && !valB) return 0;
                        if (!valA) return 1;
                        if (!valB) return -1;
                        let comparison = 0;
                        if (sortField === 'title') comparison = valA.localeCompare(valB, 'zh-Hant');
                        else comparison = new Date(valA) - new Date(valB);
                        return sortOrder === 'asc' ? comparison : -comparison;
                    });
                }
                const totalItems = filtered.length;
                const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE);
                const items = filtered.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE);
                return { items, totalItems, totalPages };
            }, [permittedDiagrams, searchTerm, activeCategory, currentPage, sortField, sortOrder]);

            const Pagination = () => {
                if (processedData.totalPages <= 1) return null;
                return (
                    <div className="flex justify-center items-center gap-2 mt-8 pt-6 border-t border-gray-200">
                        <button onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="p-2 rounded-md hover:bg-gray-100 disabled:opacity-30"><Icons.ChevronLeft /></button>
                        <span className="text-sm text-gray-600">第 {currentPage} / {processedData.totalPages} 頁</span>
                        <button onClick={() => setCurrentPage(p => Math.min(processedData.totalPages, p + 1))} disabled={currentPage === processedData.totalPages} className="p-2 rounded-md hover:bg-gray-100 disabled:opacity-30"><Icons.ChevronRight /></button>
                    </div>
                );
            };

            const SidebarItem = ({ id, label, icon, count, isActiveOverride }) => (
                <button onClick={() => { setActiveCategory(id); setIsMobileMenuOpen(false); }} className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors mb-1 ${(activeCategory === id || isActiveOverride) ? 'bg-blue-50 text-blue-700' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'}`}>
                    {icon}
                    <span className="flex-grow text-left truncate">{label}</span>
                    {count !== undefined && <span className="text-xs text-gray-400 bg-white px-1.5 py-0.5 rounded border border-gray-100">{count}</span>}
                </button>
            );

            return (
                <div className="min-h-screen bg-gray-50 flex flex-col">
                    <header className="bg-white border-b border-gray-200 sticky top-0 z-30 h-16 flex items-center px-4 justify-between lg:hidden">
                        <div className="flex items-center gap-2">
                            <img src="https://2blog.ilc.edu.tw/linkc/wp-content/uploads/sites/141/2024/12/linkc-logo.png" alt="Logo" className="h-10 w-10 rounded-lg object-contain" />
                            <h1 className="font-bold text-gray-800">Mindomo Tags</h1>
                        </div>
                        <button onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} className="p-2 text-gray-600 hover:bg-gray-100 rounded-md"><Icons.Menu /></button>
                    </header>

                    <div className="flex flex-1 overflow-hidden relative">
                        <aside className={`fixed inset-y-0 left-0 z-20 w-64 bg-white border-r border-gray-200 transform transition-transform duration-200 ease-in-out lg:translate-x-0 lg:static lg:h-auto ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'}`}>
                            <div className="h-full flex flex-col">
                                <div className="hidden lg:flex h-16 items-center px-6 border-b border-gray-100">
                                    <div className="flex items-center gap-3 text-blue-700">
                                        <img src="https://2blog.ilc.edu.tw/linkc/wp-content/uploads/sites/141/2024/12/linkc-logo.png" alt="Logo" className="h-10 w-10 rounded-lg object-contain" />
                                        <span className="font-bold text-xl">Mindomo</span>
                                    </div>
                                </div>
                                <div className="flex-1 overflow-y-auto custom-scrollbar p-4">
                                    <div className="mb-6">
                                        <p className="px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">儀表板</p>
                                        <SidebarItem id="recent" label="最近開啟" icon={<Icons.Clock />} />
                                        <SidebarItem id="all" label="全部檔案" icon={<Icons.Grid />} count={permittedDiagrams.length} />
                                    </div>
                                    
                                    <div className="mb-6">
                                        <div className="flex items-center justify-between px-3 mb-2">
                                            <p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">標籤分類 (#Tag)</p>
                                            {(isTagExpanded || sortedTags.length > 8) && (
                                                <div className="relative">
                                                    <input 
                                                        type="text" 
                                                        placeholder="篩選..." 
                                                        value={tagSearchQuery}
                                                        onChange={(e) => setTagSearchQuery(e.target.value)}
                                                        className="w-24 text-xs bg-gray-100 border border-transparent focus:bg-white focus:border-blue-300 rounded px-1.5 py-0.5 outline-none transition-colors"
                                                    />
                                                </div>
                                            )}
                                        </div>

                                        <div className="px-3 flex flex-wrap gap-2">
                                            {visibleTags.length === 0 && <p className="text-xs text-gray-400 italic w-full">沒有發現標籤</p>}
                                            {visibleTags.map(tag => {
                                                const isActive = activeCategory === tag.name;
                                                return (
                                                    <button 
                                                        key={tag.name}
                                                        onClick={() => { setActiveCategory(tag.name); setIsMobileMenuOpen(false); }}
                                                        className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium transition-colors border ${
                                                            isActive 
                                                                ? 'bg-blue-600 text-white border-blue-600 shadow-sm' 
                                                                : 'bg-white text-gray-600 border-gray-200 hover:border-blue-300 hover:text-blue-600'
                                                        }`}
                                                        title={`${tag.name} (${tag.count})`}
                                                    >
                                                        <span className="truncate max-w-[80px]">{tag.name}</span>
                                                        <span className={`ml-1.5 text-[10px] ${isActive ? 'text-blue-100' : 'text-gray-400'}`}>{tag.count}</span>
                                                    </button>
                                                );
                                            })}
                                        </div>
                                        
                                        {sortedTags.length > 10 && !tagSearchQuery && (
                                            <button 
                                                onClick={() => setIsTagExpanded(!isTagExpanded)}
                                                className="w-full text-center text-xs text-blue-500 hover:text-blue-700 mt-2 py-1 transition-colors"
                                            >
                                                {isTagExpanded ? '收合標籤' : `顯示全部 (${sortedTags.length})`}
                                            </button>
                                        )}
                                    </div>

                                    <div>
                                        <p className="px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">其他</p>
                                        <SidebarItem id="uncategorized" label="未分類 (無標籤)" icon={<Icons.File />} count={permittedDiagrams.filter(d => !d.tags || d.tags.length === 0).length} />
                                    </div>
                                </div>
                                <div className="p-4 border-t border-gray-100 bg-gray-50 space-y-2">
                                    {/* 權限切換按鈕 */}
                                    <button 
                                        onClick={() => isAuthorized ? setIsAuthorized(false) : setShowPasswordModal(true)} 
                                        className={`w-full flex items-center justify-center gap-2 text-xs py-2 px-2 border rounded transition-colors ${isAuthorized ? 'bg-red-50 border-red-200 text-red-600 hover:bg-red-100' : 'bg-white border-gray-300 text-gray-600 hover:bg-gray-50'}`}
                                    >
                                        {isAuthorized ? <Icons.Unlock /> : <Icons.Lock />}
                                        {isAuthorized ? '登出管理員' : '管理員登入'}
                                    </button>

                                    <button onClick={() => setUseRealApi(!useRealApi)} className="w-full text-xs py-1.5 px-2 border border-gray-300 rounded bg-white text-gray-600 hover:bg-gray-50">
                                        {useRealApi ? '切換演示模式' : '切換 API 模式'}
                                    </button>
                                    
                                    {useRealApi && isAuthorized && (
                                        <div className="grid grid-cols-1 gap-2">
                                            <button onClick={() => setShowDebug(!showDebug)} className={`flex items-center justify-center gap-1 text-xs py-1.5 px-2 border border-gray-200 rounded bg-white transition-colors ${showDebug ? 'text-green-600 bg-green-50 border-green-200' : 'text-gray-500 hover:text-gray-700'}`}>
                                                <Icons.Bug />
                                                {showDebug ? '關閉除錯' : '除錯模式'}
                                            </button>
                                        </div>
                                    )}
                                </div>
                            </div>
                        </aside>
                        {isMobileMenuOpen && <div onClick={() => setIsMobileMenuOpen(false)} className="fixed inset-0 z-10 bg-gray-800 bg-opacity-50 lg:hidden"></div>}

                        <main className="flex-1 overflow-y-auto custom-scrollbar h-full bg-gray-50/50 w-full">
                            <div className="max-w-5xl mx-auto px-4 sm:px-8 py-8">
                                <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
                                    <div>
                                        <div className="flex items-center gap-2 mb-1">
                                            <h2 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
                                                {activeCategory === 'recent' ? '最近修改' : 
                                                 activeCategory === 'all' ? '全部檔案' : 
                                                 activeCategory === 'uncategorized' ? '未分類檔案' : 
                                                 `# ${activeCategory}`}
                                            </h2>
                                            {!isAuthorized && (
                                                <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
                                                    訪客模式 (#share)
                                                </span>
                                            )}
                                        </div>
                                        <p className="text-sm text-gray-500">共 {processedData.totalItems} 個項目</p>
                                    </div>
                                    <div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
                                        <div className="relative group flex-1">
                                            <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"><Icons.Search /></div>
                                            <input 
                                                type="text" 
                                                value={searchTerm} 
                                                onChange={(e) => setSearchTerm(e.target.value)} 
                                                className="block w-full pl-10 pr-10 py-2 border border-gray-200 rounded-lg bg-white focus:ring-2 focus:ring-blue-500 outline-none text-sm" 
                                                placeholder="搜尋... (如 #工作 & #急件)" 
                                            />
                                            <div className="absolute inset-y-0 right-0 pr-3 flex items-center cursor-help has-tooltip">
                                                <div className="text-gray-400 hover:text-blue-500"><Icons.Info /></div>
                                                <div className="tooltip">
                                                    <p className="font-bold mb-1 text-blue-300">進階標籤搜尋語法:</p>
                                                    <ul className="list-disc list-inside space-y-1">
                                                        <li><code className="bg-gray-700 px-1 rounded">&</code> (AND): 同時包含</li>
                                                        <li><code className="bg-gray-700 px-1 rounded">|</code> (OR): 包含任一</li>
                                                        <li><code className="bg-gray-700 px-1 rounded">!</code> (NOT): 排除</li>
                                                        <li><code className="bg-gray-700 px-1 rounded">()</code>: 優先權</li>
                                                    </ul>
                                                </div>
                                            </div>
                                        </div>
                                        {activeCategory !== 'recent' && (
                                            <>
                                                <div className="h-8 w-px bg-gray-300 hidden sm:block mx-1"></div>
                                                <div className="flex gap-2">
                                                    <div className="relative">
                                                        <select value={sortField} onChange={(e) => setSortField(e.target.value)} className="appearance-none w-full sm:w-32 bg-white border border-gray-200 text-gray-700 text-sm py-2 pl-3 pr-8 rounded-lg focus:ring-2 focus:ring-blue-500 cursor-pointer outline-none">
                                                            <option value="modified_at">最近修改</option>
                                                            <option value="created_at">創建時間</option>
                                                            <option value="title">檔案名稱</option>
                                                        </select>
                                                        <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-500"><Icons.ArrowDown /></div>
                                                    </div>
                                                    <button onClick={() => setSortOrder(o => o === 'asc' ? 'desc' : 'asc')} className="p-2 bg-white border border-gray-200 rounded-lg text-gray-600 hover:text-blue-600">{sortOrder === 'asc' ? <Icons.SortAsc /> : <Icons.SortDesc />}</button>
                                                </div>
                                            </>
                                        )}
                                        {useRealApi && <button onClick={fetchDiagrams} disabled={loading} className="p-2 bg-white border border-gray-200 rounded-lg text-blue-600 hover:bg-blue-50"><Icons.Refresh spin={loading} /></button>}
                                    </div>
                                </div>
                                
                                {/* Main Content Area - Grid/Cards */}
                                {error && <div className="mb-6 p-4 bg-red-50 text-red-700 rounded-lg border border-red-100">{error}</div>}
                                
                                {processedData.totalItems === 0 ? (
                                    <div className="text-center py-20 animate-fade-in">
                                        <div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gray-100 mb-4 text-gray-300"><Icons.Grid /></div>
                                        <h3 className="text-lg font-medium text-gray-900">沒有檔案</h3>
                                        <p className="text-gray-500 mt-2 text-sm">
                                            {isAuthorized ? "確認是否有在 Mindomo 描述欄位加上 #標籤 ?" : "目前沒有公開分享 (#share) 的檔案"}
                                        </p>
                                    </div>
                                ) : (
                                    <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
                                        {processedData.items.map((item) => (
                                            <div key={item.id} className="group bg-white rounded-xl border border-gray-200 p-5 transition-all duration-200 card-hover flex flex-col h-full relative overflow-hidden">
                                                <div className="absolute top-0 left-0 w-1 h-full bg-blue-500 opacity-0 group-hover:opacity-100 transition-opacity"></div>
                                                <div className="flex justify-between items-start mb-3">
                                                    <div className="p-2.5 bg-blue-50 text-blue-600 rounded-lg group-hover:bg-blue-600 group-hover:text-white transition-colors"><Icons.File /></div>
                                                    <div className="flex gap-2">
                                                        {useRealApi && <a href={`https://www.mindomo.com/mindmap/${item.id}`} target="_blank" className="text-xs font-medium text-blue-600 bg-blue-50 px-2.5 py-1.5 rounded-full hover:bg-blue-100 transition-colors flex items-center">開啟</a>}
                                                    </div>
                                                </div>
                                                
                                                <h3 className="text-lg font-bold text-gray-900 mb-2 line-clamp-2 leading-snug group-hover:text-blue-600 transition-colors">{item.title}</h3>
                                                
                                                {/* 顯示標籤區塊 */}
                                                <div className="flex flex-wrap gap-1 mb-4 min-h-[24px]">
                                                    {item.tags && item.tags.length > 0 ? (
                                                        item.tags.map(tag => (
                                                            <span key={tag} onClick={(e) => { e.stopPropagation(); setActiveCategory(tag); }} className="cursor-pointer inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 hover:bg-blue-100 hover:text-blue-600 transition-colors">
                                                                #{tag}
                                                            </span>
                                                        ))
                                                    ) : (
                                                        <span className="text-xs text-gray-400 italic">無標籤</span>
                                                    )}
                                                </div>

                                                <div className="mt-auto pt-4 border-t border-gray-50 space-y-2">
                                                    <div className="flex items-center text-xs text-gray-500">
                                                        <Icons.Clock />
                                                        <span className="ml-2">
                                                            {item.modified_at ? `最後修改於 ${formatDate(item.modified_at)}` : '無修改日期'}
                                                        </span>
                                                    </div>
                                                    
                                                    <div className="flex gap-2 pt-2 border-t border-gray-100 justify-end">
                                                        <button onClick={(e) => handleDownload(e, item, 'md')} disabled={downloadingState && downloadingState.id === item.id} className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 bg-gray-50 hover:bg-gray-100 rounded transition-colors disabled:opacity-50" title="下載 Markdown (.md)">{downloadingState && downloadingState.id === item.id && downloadingState.format === 'md' ? <Icons.Refresh spin={true} /> : <span>MD</span>}</button>
                                                        <button onClick={(e) => handleDownload(e, item, 'mm')} disabled={downloadingState && downloadingState.id === item.id} className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 bg-gray-50 hover:bg-gray-100 rounded transition-colors disabled:opacity-50" title="下載 Freemind (.mm)">{downloadingState && downloadingState.id === item.id && downloadingState.format === 'mm' ? <Icons.Refresh spin={true} /> : <span>MM</span>}</button>
                                                        <button onClick={(e) => handleDownload(e, item, 'pdf')} disabled={downloadingState && downloadingState.id === item.id} className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 bg-gray-50 hover:bg-gray-100 rounded transition-colors disabled:opacity-50" title="下載 PDF (.pdf)">{downloadingState && downloadingState.id === item.id && downloadingState.format === 'pdf' ? <Icons.Refresh spin={true} /> : <span>PDF</span>}</button>
                                                    </div>
                                                </div>
                                            </div>
                                        ))}
                                    </div>
                                )}
                                <Pagination />
                                
                                {/* 密碼輸入視窗 */}
                                {showPasswordModal && (
                                    <PasswordModal 
                                        onSubmit={() => {
                                            setIsAuthorized(true);
                                            setShowPasswordModal(false);
                                        }}
                                        onClose={() => setShowPasswordModal(false)}
                                    />
                                )}

                                {useRealApi && rawData && showDebug && (
                                    <div className="mt-16 p-6 bg-gray-900 text-gray-300 rounded-xl shadow-lg animate-fade-in">
                                        <div className="flex items-center justify-between mb-4 border-b border-gray-700 pb-4">
                                            <div className="flex items-center gap-2">
                                                <Icons.Bug />
                                                <h3 className="text-lg font-bold text-white">API 資料除錯 (Debug Info)</h3>
                                            </div>
                                            <button onClick={() => setShowDebug(false)} className="text-xs text-gray-500 hover:text-white">關閉</button>
                                        </div>
                                        <p className="text-sm mb-4 text-gray-400">
                                            請檢查 JSON 中 <code>description</code> 欄位是否有包含 #標籤。
                                        </p>
                                        
                                        <div className="space-y-6">
                                            <div>
                                                <h4 className="text-xs font-bold text-blue-400 uppercase tracking-wider mb-2">原始資料 (前 1 筆範例)</h4>
                                                <pre className="bg-black p-4 rounded-lg text-xs overflow-x-auto font-mono border border-gray-800 text-green-400">
                                                    {JSON.stringify(Array.isArray(rawData) ? rawData[0] : (rawData.diagrams ? rawData.diagrams[0] : rawData), null, 2)}
                                                </pre>
                                            </div>
                                            
                                            <div>
                                                <h4 className="text-xs font-bold text-blue-400 uppercase tracking-wider mb-2">資料結構統計</h4>
                                                <div className="bg-black p-4 rounded-lg text-xs font-mono border border-gray-800">
                                                    <p>總筆數: {Array.isArray(rawData) ? rawData.length : (rawData.diagrams ? rawData.diagrams.length : 0)}</p>
                                                    <p>包含 'description' 欄位的筆數: {(Array.isArray(rawData) ? rawData : (rawData.diagrams || [])).filter(i => i.description).length}</p>
                                                    <p>偵測到的標籤總數: {sortedTags.length}</p>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                )}
                            </div>
                        </main>
                    </div>
                </div>
            );
        }

        const root = ReactDOM.createRoot(document.getElementById('root'));
        root.render(<App />);
    </script>
</body>
</html>